/*
 * Copyright 2013 Agustina Martinez-Garcia

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
 */
package uk.ljmu.metadata.schemasutilities;

import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLConnection;
import java.util.Iterator;
import java.util.List;

import org.apache.log4j.Logger;
import org.dom4j.Attribute;
import org.dom4j.Document;
import org.dom4j.DocumentHelper;
import org.dom4j.Element;
import org.dom4j.Namespace;
import org.dom4j.Node;
import org.dom4j.QName;

import uk.ljmu.metadata.utilities.Constants;
import uk.ljmu.metadata.utilities.MetadataUtilities;
//import uk.ljmu.metadata.utilities.RdfFunctions;
// TODO THIS CLASS REPLACES RdfFunctions Class under UTILITIES package so check of all the references to the old class and change them
public class SchemasProcessing {
	
	private static final Logger log = Logger.getLogger(SchemasProcessing.class);
	/**
	 * Check if a given property ia already specified in an RDF record
	 * 
	 * @param elem XML element to check
	 * @param propertyName name of the property to check existence
	 * @param propertyValue value of the property
	 
	 * @return true if already present, false otherwise
	 */
	public static boolean checkPropertyExistence(Element elem, String propertyName, String propertyValue)
	{
		//int prefixIndex = -1;
		//prefixIndex = propertyName.indexOf(Constants.NS_SEP);

		//if (prefixIndex >= 0)
		//	propertyName = propertyName.substring(prefixIndex + 1, propertyName.length());
		Iterator<?> iter = elem.elementIterator(propertyName);

		while (iter.hasNext())
		{
			Element elProp = (Element) iter.next();
			String value = elProp.attributeValue("rdf:resource");
			
			//This statement is already present in the RELS_EXT datastream
			if (value != null && value.compareToIgnoreCase(propertyValue) == 0)
			{
				//Don't add it and return
				log.info("Duplicate property: " + propertyName + ", " + propertyValue);
				return true;					
			}
		}
		return false;
	}
	//SCHEMA AND RDF DOCUMENT MANAGEMENT METHODS
	/**
	 * Create an empty RDF document with one Description Element
	 * @param objectPID pid of the object being described, rdf:about value
	 * @param prefixs to include in the document
	 *
	 * @return RDF document
	 */
	public static Document contructRDFDocument(String objectPID, List<String> prefixs)
	{
		//Create document
		Element root = DocumentHelper.createElement( "rdf:RDF" );
		root.addNamespace("rdf", MetadataUtilities.RELS_MAP.get("rdf"));
		if (prefixs != null && prefixs.size() > 0) {
			for (String pref : prefixs)	{
					String ns = MetadataUtilities.MDSchemasUrl.get(pref);
					if (ns == null)
						continue;
					root.addNamespace(pref, ns);
				}
			}

		Element description = DocumentHelper.createElement("rdf:Description");
		description.addAttribute("rdf:about", !objectPID.startsWith("info") ? MetadataUtilities.OBJECTID + objectPID : objectPID);
		root.add(description);
		Document doc = DocumentHelper.createDocument(root);
		
		return doc;
	}
	
	/**
	 * Create an empty RDF document
	 * @param prefixes and namespaces to include in the document  
	 *
	 * @return RDF document
	 */
	public static Document contructRDFDocument(List<String> prefixs)
	{
		//Create document
		Element root = DocumentHelper.createElement( "rdf:RDF" );
		root.addNamespace("rdf", MetadataUtilities.RELS_MAP.get("rdf"));
		if (prefixs != null && prefixs.size() > 0) {
			for (String pref : prefixs)	{
					String ns = MetadataUtilities.MDSchemasUrl.get(pref);
					if (ns == null)
						continue;
					root.addNamespace(pref, ns);
				}
			}
		Document doc = DocumentHelper.createDocument(root);
		
		return doc;
	}
	/**
	 * Add a property to an existing RDF description record
	 * @param ns the namespace of the property to add 
	 * @param propertyName string with the property name
	 * @param propertyValue string with the value of the property
	 * @param propertyType string containing the property range
	 * @param xmlContents String containing the actual record in which to add the new property
	 * 
	 * @return Document with the properties added
	 */
	//public static Document addRDFProperty(String ns, String propertyName, String propertyValue, String propertyType, String xmlContents)
	public static boolean addRDFProperty(String ns, String propertyName, String propertyValue, String propertyType, Document xmlDoc)
	{
		//Create document
		//Document doc = null;
		//try 
		//{
		//	doc = DocumentHelper.parseText(xmlContents);
		//} catch (DocumentException e) {
		//	log.fatal(e.getMessage());
		//}
		//if (doc != null)
		if (xmlDoc != null)	{
			//AMG (16/11) get root element
			Element root = xmlDoc.getRootElement();
			//AMG (16/11) check namespace
			String nsURL = MetadataUtilities.MDSchemasUrl.get(ns);
			if (nsURL == null) {
				//AMG (16/11) Namespace not valid
				log.fatal("Namespace prefix not valid: " + ns);
				return false;
			}
			Namespace namespace = new Namespace(ns, nsURL);
			root.addNamespace(ns, nsURL);
			Element desc = root.element("rdf:Description");
			if (desc == null) {
				desc = root.element("Description");
				if (desc == null) {
					log.fatal("Error while creating XML Document: Document does not have a Description element");
					return false;
				}
			}
			QName qname; 
			//AMG - Already Qualified
			if (!propertyName.contains(Constants.NS_SEP))
				qname = new QName(propertyName, namespace);
			else qname = new QName(propertyName);
			
			Element property = DocumentHelper.createElement(qname);
			if (propertyType == null || propertyType == "") {
				//AMG - We treat it as a literal
				property.setText(propertyValue);
			} else {
				//AMG (16/11) WILL HAVE TO MAKE USE OF THE ONTOLOGIES TO GUESS THE RDF ELEMENT NEEDED HERE
				if (propertyType.equalsIgnoreCase(Constants.RDFS_TYPE_CLASS) || propertyType.equalsIgnoreCase(Constants.RDFS_TYPE_RES)) {
					//AMG - Check valid URI
					if (checkValidURI(propertyValue))
						property.addAttribute("rdf:resource", propertyValue);
					else {
						// TODO Leave it like this? Not valid URI then returns OK but property not added
						log.info("In SchemasProcessing.addRDFProperty: property not added since the value it is not a proper URI - " + propertyValue);
						return true; //Property NOT added
					}
				} else property.setText(propertyValue);
			}
			//Add property
			desc.add(property);

			return true;
		} else {
			log.fatal("In SchemasProcessing.addRDFProperty:: error while adding rdf property, document is NULL");
			return false;
		}
	}
	/**
	 * Adds a list of namespaces to and RDF root element
	 * @param doc RDF document to add namespaces
	 * @param prefixes
	 */
	public static void addNamespacesToDocument(Document doc, List<String> prefixes) {
		Element root = doc.getRootElement();
		if (prefixes != null && prefixes.size() > 0) {
			for (String pref : prefixes)	{
					String ns = MetadataUtilities.MDSchemasUrl.get(pref);
					if (ns == null)
						continue;
					root.addNamespace(pref, ns);
				}
			}
	}
	
	/**
	 * Adds a list of namespaces to and RDF root element
	 * @param doc RDF document to add namespaces
	 * @param prefixes
	 */
	public static void addNamespacesToDocument(Document doc, String prefix) {
		Element root = doc.getRootElement();
		String ns = MetadataUtilities.MDSchemasUrl.get(prefix);
		if (ns == null)
			return;
		root.addNamespace(prefix, ns);
	}
	
	//VARIOUS UTILITIES
	/**
	 * Check if the spreadsheet header is a valid header: element of a metadata schema
	 * of special types of datastreams in Fedora.
	 * 
	 * @param ds_name string containing the header name
	 * 
	 * @return true if valid header, false otherwise
	 */
	public static boolean checkIsDataStream(String ds_name)
	{
		//Check if the header belongs to a Metadata schema
		int index = ds_name.indexOf(Constants.NS_SEP);
		String schemaName, typeName = "";

		if (index >= 0)
		{
			schemaName = ds_name.substring(0, index);
			typeName = ds_name.substring(index + 1, ds_name.length());
		}//The datastrean is neither datastream or schema element
		else return false;

		//Check if it's an schema element
		String schemaElem = MetadataUtilities.MDSchemasUrl.get(schemaName);
		if (schemaElem != null)
			return false;
		//Check if it's a valid datastream type
		MetadataUtilities.ValueType elem = MetadataUtilities.ValueType.valueOf(typeName.toUpperCase());

		if (elem != null)
			return true;
		else return false;		
	}

	/**
	 * Method to check if an URL is valid
	 * 
	 * @param urlStr string containing the URL to check
	 * @return true if valid, false otherwise
	 */
	public static boolean validateURL(String urlStr)
	{
		if (urlStr.equals(""))
			return false;
		URL url;
		try {
			url = new URL(urlStr);
			URLConnection connection = url.openConnection();

			if (connection instanceof HttpURLConnection) {
				HttpURLConnection httpConnection = (HttpURLConnection) connection;
				httpConnection.connect();
				int response = httpConnection.getResponseCode();
				log.info("Response: " + String.valueOf(response));
			}

			return true;
		} catch (MalformedURLException e) {
			return false;
		} catch (IOException e) {
			return false;
		}
			
	}
	
	public static boolean checkValidURI(String strURI) {
		try {
			URI uri = new URI(strURI);
			return uri.isAbsolute();
		} catch (URISyntaxException e) {
			return false;
		}
	}
//	public static boolean checkValidHttpURI(String strURI) {
//		String URL_REGEX = @"(?#WebOrIP)((?#protocol)((http|https):\\\/\\\/)?(?#subDomain)(([a-zA-Z0-9]+\\\.(?#domain)[a-zA-Z0-9\\\-]+(?#TLD)(\\\.[a-zA-Z]+){1,2})|(?#IPAddress)((25[0-5]|2[0-4][0-9]|[0-1]{1}[0-9]{2}|[1-9]{1}[0-9]{1}|[1-9])\.(25[0-5]|2[0-4][0-9]|[0-1]{1}[0-9]{2}|[1-9]{1}[0-9]{1}|[1-9]|0)\.(25[0-5]|2[0-4][0-9]|[0-1]{1}[0-9]{2}|[1-9]{1}[0-9]{1}|[1-9]|0)\.(25[0-5]|2[0-4][0-9]|[0-1]{1}[0-9]{2}|[1-9]{1}[0-9]{1}|[0-9])))+(?#Port)(:[1-9][0-9]*)?)+(?#Path)((\/((?#dirOrFileName)[a-zA-Z0-9_\-\%\~\+]+)?)*)?(?#extension)(\.([a-zA-Z0-9_]+))?(?#parameters)(\?([a-zA-Z0-9_\-]+\=[a-z-A-Z0-9_\-\%\~\+]+)?(?#additionalParameters)(\&([a-zA-Z0-9_\-]+\=[a-z-A-Z0-9_\-\%\~\+]+)?)*)?";
//		Regex urlTest = new Regex(URL_REGEX, RegexOptions.IgnoreCase | RegexOptions.Compiled);
//		return urlTest.IsMatch(url);
//	}
	public static boolean isPartOfDCMI(String strValue) {
		//AMG - Check if it is a url
		String key = "";
		if (strValue.startsWith("http")) {
			int index = strValue.lastIndexOf("/");
			if (index == -1) {
				//AMG - Bad URL
				return false;
			}
			key = strValue.substring(index + 1, strValue.length());
		} else key = strValue;

		return MetadataUtilities.DCMI_ELEMENTS.containsKey(key.toLowerCase());
	}
	
	public static Document constructRelsIntDocument() {
		
		Element root = DocumentHelper.createElement( "rdf:RDF" );
		root.addNamespace("rdf", MetadataUtilities.RELS_MAP.get("rdf"));
		root.addNamespace("rel", MetadataUtilities.RELS_MAP.get("rel"));
		Document doc = DocumentHelper.createDocument(root);
		
		return doc;
	}
	/**
	 * Adds a description element for a Rels-int fedora statement in an RDF document
	 * @param ns namespace of the property to be added
	 * @param xmlDoc Actual RDF document instance
	 * @param descriptionElement identifier of the Description element to add the property
	 * @param propertyName
	 * @param value
	 * @param propertyType
	 * @return
	 */
	//public static Document addRelsIntRecord(String ns, String xmlContents, String descriptionElement, String propertyName, String value, String propertyType) {
	public static boolean addRelsIntRecord(String ns, Document xmlDoc, String descriptionElement, String propertyName, String value, String propertyType) {
		boolean descFound = false;
		
		if (xmlDoc != null)	{
			//AMG get root element
			Element root = xmlDoc.getRootElement();
			//AMG check namespace
			String nsURL = MetadataUtilities.MDSchemasUrl.get(ns);
			if (nsURL == null) {
				//AMG Namespace not valid
				log.fatal("Namespace prefix not valid: " + ns);
				return false;
			}
			Namespace namespace = new Namespace(ns, nsURL);
			root.addNamespace(ns, nsURL);
			//AMG - (25/01/11) Improve this: look for existing description records matching the ID if found add the property to it,
			//if not create a new one
			Element description = null;
			Iterator<?> descIterator = root.elementIterator("rdf:Description");
			if (!descIterator.hasNext()) {
				description = DocumentHelper.createElement("rdf:Description");
				description.addAttribute("rdf:about", descriptionElement);
			} else {
				while (descIterator.hasNext()) {
					description = (Element) descIterator.next();
					if (description.attribute("rdf:about").getText().equals(descriptionElement)) {
						descFound = true;
						break;
					}
				}
				//NOT FOUND
				if (!descFound) {
					description = DocumentHelper.createElement("rdf:Description");
					description.addAttribute("rdf:about", descriptionElement);
				}
			}
			
			QName qname; 
			//AMG - Already Qualified
			if (!propertyName.contains(Constants.NS_SEP))
				qname = new QName(propertyName, namespace);
			else qname = new QName(propertyName);
			
			Element property = DocumentHelper.createElement(qname);
			if (propertyType == null || propertyType == "") {
				//AMG - We treat it as a literal
				property.setText(value);
			} else {
				//AMG WILL HAVE TO MAKE USE OF THE ONTOLOGIES TO GUESS THE RDF ELEMENT NEEDED HERE
				if (propertyType.equalsIgnoreCase(Constants.RDFS_TYPE_CLASS) || propertyType.equalsIgnoreCase(Constants.RDFS_TYPE_RES)) {
					//AMG - Check valid URI
					if (checkValidURI(value))
						property.addAttribute("rdf:resource", value);
					else {
						// TODO Leave it like this? Not valid URI then returns OK but property not added
						log.info("In SchemasProcessing.addRelsIntRecord: property not added since the value it is not a proper URI - " + value);
						return true;
					}
				} else property.setText(value);
			}
			//Add property
			description.add(property);
			if (!descFound)
			    root.add(description);
			return true;
		} 
		else {
			log.fatal("In SchemasProcessing.addRelsIntRecord, error NULL document as paremeter");
			return false;
		}		
	}
	/**
	 * MANAGE NAMESPACE IN RDF WITH DOM4J
	 */
    public static void removeAllNamespaces(Document doc) {
        Element root = doc.getRootElement();
        if (root.getNamespace() !=
                Namespace.NO_NAMESPACE) {            
                removeNamespaces(root.content());
        }
    }

    public static void unfixNamespaces(Document doc, Namespace original) {
        Element root = doc.getRootElement();
        if (original != null) {
            setNamespaces(root.content(), original);
        }
    }

    public static void setNamespace(Element elem, Namespace ns) {

        elem.setQName(QName.get(elem.getName(), ns,
                elem.getQualifiedName()));
    }

    /**
     *Recursively removes the namespace of the element and all its
    children: sets to Namespace.NO_NAMESPACE
     */
    public static void removeNamespaces(Element elem) {
        setNamespaces(elem, Namespace.NO_NAMESPACE);
    }

    /**
     *Recursively removes the namespace of the list and all its
    children: sets to Namespace.NO_NAMESPACE
     */
    public static void removeNamespaces(List<?> l) {
        setNamespaces(l, Namespace.NO_NAMESPACE);
    }

    /**
     *Recursively sets the namespace of the element and all its children.
     */
    public static void setNamespaces(Element elem, Namespace ns) {
        setNamespace(elem, ns);
        setNamespaces(elem.content(), ns);
    }

    /**
     *Recursively sets the namespace of the List and all children if the
    current namespace is match
     */
    public static void setNamespaces(List<?> l, Namespace ns) {
        Node n = null;
        for (int i = 0; i < l.size(); i++) {
            n = (Node) l.get(i);

            if (n.getNodeType() == Node.ATTRIBUTE_NODE) {
                ((Attribute) n).setNamespace(ns);
            }
            if (n.getNodeType() == Node.ELEMENT_NODE) {
                setNamespaces((Element) n, ns);
            }            
        }
    }
}
